package com.hy.dp.bagProblem.ZeroOnebag;

import java.util.Arrays;

public class DivideSubsets {


    /**
     * 416. 分割等和子集
     * 力扣题目链接
     *
     * 题目难易：中等
     *
     * 给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集，使得两个子集的元素和相等。
     *
     * 注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200
     *
     * 示例 1: 输入: [1, 5, 11, 5] 输出: true 解释: 数组可以分割成 [1, 5, 5] 和 [11].
     *
     * 示例 2: 输入: [1, 2, 3, 5] 输出: false 解释: 数组不能分割成两个元素和相等的子集.
     *
     * 提示：
     *
     * 1 <= nums.length <= 200
     * 1 <= nums[i] <= 100
     *
     * 思路
     * 这道题目初步看，是如下两题几乎是一样的，大家可以用回溯法，解决如下两题
     *
     * 698.划分为k个相等的子集
     * 473.火柴拼正方形
     *
     * 这道题目是要找是否可以将这个数组分割成两个子集，使得两个子集的元素和相等。
     *
     * 那么只要找到集合里能够出现 sum / 2 的子集总和，就算是可以分割成两个相同元素和子集了。
     *
     * 01背包问题
     * 背包问题，大家都知道，有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i]，得到的价值是value[i] 。每件物品只能用一次，求解将哪些物品装入背包里物品价值总和最大。
     *
     * 背包问题有多种背包方式，常见的有：01背包、完全背包、多重背包、分组背包和混合背包等等。
     *
     * 要注意题目描述中商品是不是可以重复放入。
     *
     * 即一个商品如果可以重复多次放入是完全背包，而只能放入一次是01背包，写法还是不一样的。
     *
     * 要明确本题中我们要使用的是01背包，因为元素我们只能用一次。
     * 回归主题：
     * 只有确定了如下四点，才能把01背包问题套到本题上来。
     *
     * 背包的体积为sum / 2
     * 背包要放入的商品（集合里的元素）重量为 元素的数值，价值也为元素的数值
     * 背包如果正好装满，说明找到了总和为 sum / 2 的子集。
     * 背包中每一个元素是不可重复放入。
     * 以上分析完，我们就可以套用01背包，来解决这个问题了。
     * 动规五部曲分析如下：
     *
     * 1. 确定dp数组以及下标的含义
     * 01背包中，dp[j] 表示： 容量为j的背包，所背的物品价值可以最大为dp[j]。
     *
     * 套到本题，dp[j]表示 背包总容量是j，最大可以凑成j的子集总和为dp[j]。
     *
     * 2. 确定递推公式
     * 01背包的递推公式为：dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
     *
     * 本题，相当于背包里放入数值，那么物品i的重量是nums[i]，其价值也是nums[i]。
     *
     * 所以递推公式：dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
     *
     * 3.dp数组如何初始化
     * 在01背包，一维dp如何初始化，已经讲过，
     *
     * 从dp[j]的定义来看，首先dp[0]一定是0。
     *
     * 如果如果题目给的价值都是正整数那么非0下标都初始化为0就可以了，如果题目给的价值有负数，那么非0下标就要初始化为负无穷。
     *
     * 这样才能让dp数组在递归公式的过程中取的最大的价值，而不是被初始值覆盖了。
     *
     * 本题题目中 只包含正整数的非空数组，所以非0下标的元素初始化为0就可以了。
     *
     * 4. 确定遍历顺序
     * 在动态规划：关于01背包问题，你该了解这些！（滚动数组）中就已经说明：如果使用一维dp数组，物品遍历的for循环放在外层，遍历背包的for循环放在内层，且内层for循环倒序遍历！
     *
     * 5. 举例推导dp数组
     * dp[j]的数值一定是小于等于j的。
     *
     * 如果dp[j] == j 说明，集合中的子集总和正好可以凑成总和j，理解这一点很重要。
     * 用例1，输入[1,5,11,5] 为例，如图：
     * 0    1   2   3   4   5   6   7   8   9   10  11
     * 0    1   1   1   1   5   6   6   6   6   10  11
     * @param num
     * @return
     */
    public boolean devidingSubsets(int [] num){
        int sum = Arrays.stream(num).sum();
        if (sum %2 == 1){
            return false;
        }
        int target = sum / 2;
        // 1.确定 dp数组以及下标的含义
        int [] dp = new int[target + 1];
        // 2. 确定递推式
        //dp[i] = Math.max(dp[i],dp[i - num[i]] + num[i])
        // 3. 初始化

        // 4. 确定遍历顺序
        // 开始01背包
        for (int i = 0; i < num.length; i++) {
             // 每一个元素一定是不可重复放入，所以从大到小遍历
            for (int j = target; j >= num[i]; j--) {
                //物品 i 的重量是 nums[i]，其价值也是 nums[i]
                dp[j] = Math.max(dp[j],dp[j - num[i]] + num[i]);
            }
            
        }
        return dp[target] == target;
    }

    public static void main(String[] args) {
        DivideSubsets divideSubsets = new DivideSubsets();
        int [] num = {1,5,11,5};
        System.out.println("res: "+divideSubsets.devidingSubsets(num));

    }
}
